<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue.js双向绑定的实现原理</title>
</head>

<body>
    <!-- 2 极简双向数据绑定 -->
    <!-- <input type="text" id="a">
    <span id="b"></span> -->

    <!-- <div id="app">
        <input type="text" v-model='text'>
        {{text}}
    </div> -->

    <!-- 4 DocumentFragment -->
    <!-- <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
    </div> -->

    <!-- 5、数据初始化绑定 -->
    <div id="app">
        <input type="text" v-model='text'>
        {{ text }}
    </div>
    <script>
        //1、访问器属性是对象的一种特殊属性 它不能直接在对象中设置 而必须通过 defineProperty() 方法单独定义
        // 访问器属性的‘值’ 比较特殊，读取或者设置访问器属性的值实际上是调用其内部特性：get 和 set 函数 
        // var obj = {}
        // Object.defineProperty(obj, 'hello', {
        //     get: function () {
        //         console.log('调用get了')
        //     },
        //     set: function (val) {
        //         console.log('set 方法被调用了,参数是' + val)
        //     }
        // })
        // obj.hello
        // obj.hello = 'hello world'
        // 注释：get和set方法的内部的指向都指向obj 这意味着get 和 set 函数可以操作对象内部的值 ，另外访问器属性会覆盖同名的普通属性，因为访问器属性会被优先访问，与其同名的普通属性则会被忽略。

        // 2、极简双向绑定的实现
        // var obj = {}
        // Object.defineProperty(obj, 'hello', {
        //     set: function (newVal) {
        //         document.getElementById('a').value = newVal
        //         document.getElementById('b').innerHTML = newVal
        //     }
        // })
        // document.addEventListener('keyup', function (e) {
        //     obj.hello = e.target.value
        // })

        // 以上实现的效果是：随文本框输入文字的变化，span 会同步显示相同的文字内容，在js或者控制台显示的修改hello 的值 ，视图会响应的更新 ，这样就实现了 view==>model  model==>view 的双向数据绑定 

        // 3、分解任务
        // 以上两个例子的实现仅仅是说明了原理,最终要实现的是如下的代码 即Vue 源码的实现格式

        // 4、 DocumentFragment 
        // DocumentFragment (文档片段)可以看做节点容器，它包含多个子节点，当我们将它插入到DOM中时，只有它的子节点会插入目标节点，所以把它看做一组节点的容器。使用DocumentFragment 处理节点，速度和性能远远优先于直接操作DOM，Vue 进行编译时，就是讲挂载目标的所有子节点劫持(真的是劫持，通过将append 方法，DOM中的节点会被自动删除)到DocumentFragment中,经过一番处理后，再将DocumentFragment 整体返回插入挂载目标。

        // var dom = nodeToFragment(document.getElementById('app'))
        // console.log(dom)

        // function nodeToFragment(node) {
        //     let flag = document.createDocumentFragment()
        //     let child;
        //     while (child = node.firstChild) {
        //         // flag.append(child) //劫持node 的所有子节点
        //         // console.log(child)
        //         flag.appendChild(child)
        //     }
        //     // console.log(flag)
        //     return flag;
        // }
        // document.getElementById('app').appendChild(dom)

        // 5、数据初始化绑定
        function nodeToFragment(node, vm) {
            console.log(node)
            console.log("nodeToFragment")
            let flag = document.createDocumentFragment()
            let child;
            while (child = node.firstChild) {
                compile(child, vm)
                flag.appendChild(child)
            }
            // console.log(flag)
            return flag;
        }

        // function compile(node, vm) {
        //     let reg = /\{\{(.*)\}\}/;
        //     // 节点类型为元素
        //     if (node.nodeType === 1) {
        //         let attr = node.attributes
        //         // 解析属性
        //         for (var i = 0; i < attr.length; i++) {
        //             if (attr[i].nodeName == 'v-model') {
        //                 let name = attr[i].nodeValue //获取v-mode 绑定的属性名
        //                 node.value = vm.data[name]; //将data 的值赋值给node
        //                 node.removeAttribute('v-model')
        //             }
        //         }
        //     }
        //     // 节点类型为text 
        //     if (node.nodeType === 3) {
        //         if (reg.test(node.nodeValue)) {
        //             let name = RegExp.$1 // 获取匹配到的字符串
        //             name = name.trim()
        //             node.nodeValue = vm.data[name] // 将data 的值赋给node
        //         }
        //     }
        // }

        // function Vue(options) {
        //     this.data = options.data
        //     const id = options.el
        //     const dom = nodeToFragment(document.getElementById(id), this);
        //     // 编译完成后，将dom 返回到app 中
        //     document.getElementById(id).appendChild(dom)
        // }
        // let vm = new Vue({
        //     el: 'app',
        //     data: {
        //         text: 'hello world'
        //     }
        // })

        // 6、响应式数据绑定 任务二实现思路 当我们在输入框输入数据的时候 首先触犯input 事件(或者keyup change 事件),在相应的事件处理程序中获取输入框的value 并赋值给vm 实例的text 属性，我们会利用defineProperty 将data 中的text 设置为vm的访问器属性，因此vm.text 赋值 就会触发set 方法，在set 方法中主要做两件事 第一是更新属性的值，第二留到任务三再说

        // function defineReactive(obj, key, val) {
        //     Object.defineProperty(obj, key, {
        //         get: function () {
        //             return val
        //         },
        //         set: function (newVal) {
        //             if (newVal === val) return
        //             val = newVal
        //             console.log(val) // 方便看效果
        //         }
        //     })
        // }

        // function observe(obj, vm) {
        //     Object.keys(obj).forEach((key) => {
        //         defineReactive(vm, key, obj[key])
        //     })
        // }

        // function Vue(options) {
        //     this.data = options.data
        //     let data = this.data
        //     observe(data, this) // 监听数据，给数据的每一个属性都设置了 get 和 set 方法 能够实现数据劫持的监听
        //     // console.log(data)
        //     let id = options.el
        //     var dom = nodeToFragment(document.getElementById(id), this) // 编译HTML
        //     // 编译完成后 将dom 返回到app中
        //     document.getElementById(id).appendChild(dom)
        // }
        // let vm = new Vue({
        //     el: 'app',
        //     data: {
        //         text: 'hello'
        //     }
        // })

        // function compile(node, vm) {
        //     var reg = /\{\{(.*)\}\}/
        //     // 节点类型为元素
        //     if (node.nodeType === 1) {
        //         let attr = node.attributes
        //         //解析属性
        //         for (let i = 0; i < attr.length; i++) {
        //             if (attr[i].nodeName == 'v-model') {
        //                 let name = attr[i].nodeValue //获取v-modebangding de shuxing 
        //                 node.addEventListener('input', function (e) {
        //                     vm[name] = e.target.value
        //                 })
        //                 node.value = vm[name]
        //                 node.removeAttribute('v-model')
        //             }
        //         }
        //     }
        //     // 节点类型为text 
        //     if (node.nodeType === 3) {
        //         if (reg.test(node.nodeValue)) {
        //             let name = RegExp.$1 // 获取匹配到的字符串
        //             name = name.trim()
        //             node.nodeValue = vm[name] //将data 的值赋给node
        //         }
        //     }
        // }

        // 以上6完成的任务是 text 属性值会与输入框的内容进行同步变化

        // 7 订阅和发布模式（subscribe&publish）
        //text 属性变化了，set 方法触发了，但是文本节点的内容并没有变化，如何让同样绑定到text的文本节点也同步变化呢，这里又有一个知识点：订阅发布模式
        //订阅发布模式(又称观察者模式)定义了一种一对多的关系，让多个观察者同事监听一个主题对象，这个主题对象的状态发生改变时就会通知所有观察者对象
        // 发布者发出通知==》主题对象收到通知并推送给订阅者==》订阅者执行响应操作

        // 一个发布者 publisher
        // let pub = {
        //     publish: () => {
        //         dep.notify()
        //     }
        // }

        // // 三个订阅者

        // let sub1 = {
        //     update: function () {
        //         console.log(1)
        //     }
        // }
        // let sub2 = {
        //     update: function () {
        //         console.log(2)
        //     }
        // }
        // let sub3 = {
        //     update: function () {
        //         console.log(3)
        //     }
        // }

        // // 一个主题对象
        // function Dep() {
        //     this.subs = [sub1, sub2, sub3]
        // }
        // Dep.prototype.notify = function () {
        //     this.subs.forEach((sub) => {
        //         sub.update()
        //     })
        // }
        // //发布者发布消息 主题对象执行notify 方法，进而触发订阅者执行update方法
        // let dep = new Dep()
        // pub.publish()

        // 之前提到的，当set 方法触发后做的第二件事就是作为发布者发出通知，：‘我是属性text 我变了’，文本节点则作为订阅者，在收到消息后执行响应的更新操作。

        // 8 双向绑定的实现
        // 回顾6的实现，每当new 一个Vue ,主要做了两件事情，：第一个是监听数据：observe(data) ,第二个是编译HTML：nodeToFragement(id)
        //在监听数据的过程中，会为data中的每一个属性生成一个主题对象dep 
        //在编译HTML的过程中，会为每个与数据绑定相关的节点生成一个订阅者watcher ,watcher 会将自己添加到响应属性的dep 中
        // 以上已经实现的是：修改输入框内容==》在回调时间函数中修改属性值==》触发属性set 方法。
        // 接下来要实现的是：发出通知dep.notify()=>触发订阅者update 方法==》更新视图。
        // 以下关键逻辑是：如何将watcher添加到关联属性dep 中

        function Watcher(vm, node, name) {
            Dep.target = this
            this.name = name
            this.node = node
            this.vm = vm
            this.update()
            Dep.target = null
        }
        Watcher.prototype = {
            update: function () {
                this.get();
                this.node.nodeValue = this.value
            },
            get: function () {
                this.value = this.vm[this.name] // 触发响应属性的get
            }
        }

        // 以上代码解析：
        // 首先，将自己赋给了一个全局变量 Dep.target
        // 其次，执行了update 方法，进而执行get 方法，get 方法读取了vm 的访问器属性，从而触发了访问器属性的get 方法，get 方法中将该Watcher 添加到了对相应的访问器属性的dep 中
        //
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub)
            },
            notify: function () {
                this.subs.forEach(sub => {
                    sub.update();
                })
            }
        }

        function defineReactive(obj, key, val) {
            var dep = new Dep();
            Object.defineProperty(obj, key, {
                get: function () {
                    // 添加订阅者 watcher 到 主题对象 Dep
                    if (Dep.target) dep.addSub(Dep.target)
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal
                    // 作为发布者发出通知
                    dep.notify()
                    console.log(val) // 方便看效果
                }
            })
        }

        function observe(obj, vm) {
            Object.keys(obj).forEach((key) => {
                defineReactive(vm, key, obj[key])
            })
        }

        function Vue(options) {
            this.data = options.data
            let data = this.data
            observe(data, this) // 监听数据，给数据的每一个属性都设置了 get 和 set 方法 能够实现数据劫持的监听
            // console.log(data)
            let id = options.el
            var dom = nodeToFragment(document.getElementById(id), this) // 编译HTML
            // 编译完成后 将dom 返回到app中
            document.getElementById(id).appendChild(dom)
        }
        let vm = new Vue({
            el: 'app',
            data: {
                text: 'hello'
            }
        })

        function compile(node, vm) {
            var reg = /\{\{(.*)\}\}/
            // 节点类型为元素
            if (node.nodeType === 1) {
                let attr = node.attributes
                //解析属性
                for (let i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        let name = attr[i].nodeValue //获取v-modebangding de shuxing 
                        node.addEventListener('input', function (e) {
                            vm[name] = e.target.value
                        })
                        node.value = vm[name]
                        node.removeAttribute('v-model')
                    }
                }
            }
            // 节点类型为text 
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    let name = RegExp.$1 // 获取匹配到的字符串
                    name = name.trim()
                    // node.nodeValue = vm[name] //将data 的值赋给node

                    new Watcher(vm, node, name)
                }
            }
        }
    </script>
</body>

</html>